在學習 JS 時,必須要知道它是單執行緒(單線程),最好理解的方法就是它一次只能做一件事。
那這就怪了,同步執行下,setTimeout
、event callback
、Http request
這些耗時、不確定觸發執行時間的操作,不就會使程式阻塞,都在等它們就飽了?
所幸可以透過同步/非同步來處理這些事件,將這些耗時、無法預期執行時間的操作以非同步處理,等執行完同步的程式碼再來處理它們。
真是可喜可賀可喜可賀~
Hmm...不對吧,你不是說 JS 是同步執行,哪來的非同步?
抓到了,你再胡說我要退訂了!
等等,讓我解釋清楚,不要讓已經夠少的訂閱歸零Q_Q
先記得一個重點: JS 的引擎是單執行緒的,瀏覽器無論在什麼時候都只有一個線程在執行 JS 程式。
這邊引用知乎上面的問答(來源找不到,歡迎幫忙補上),並盡量修改成台灣的用語,
瀏覽器的內核是多線程的,它們在內核控制下相互配合以保持同步,至少實現三個常駐線程: 1. JavaScript 引擎線程、2. GUI 渲染線程、3. 瀏覽器事件觸發線程。
3.事件觸發線程: 當一個事件被觸發時,該線程會把事件添加到待處理隊列的隊尾,等待 JavaScript 引擎的處理。
這些事件可來自 JavaScript 引擎當前執行的程式碼,如 setTimeout、也可來自瀏覽器內核的其他線程如滑鼠點擊、Ajax 異步請求等,但由於 JavaScript 的單線程關係,所有這些事件都得排隊等待 JavaScript 引擎處理(當線程中沒有執行任何同步程式碼的前提下才會執行異步程式碼)。
看到上面的描述,我們統整出兩個重點,
setTimeout
、event
、ajax
,或其他無法預期執行時間的操作,都會以非同步處理。也就是會先被丟到事件佇列(Queue)
,等到同步執行的程式碼執行完,才會去處理那些被放到佇列中的任務。結合昨天講的,執行 function 時,會依執行順序把 function 丟到 主執行緒(stack)
中,等到 stack 中的任務都執行完畢,才會將 事件佇列(Queue)
中待執行的任務拉到 主執行緒(stack)
中執行,執行完畢(stack清空)後,再到 事件佇列(Queue)
中查看是否還有待執行任務,這個查看的過程就稱為 Event Loop
。
講了那麼多,先直接來看個範例吧,
請問印出的順序為何?
setTimeout(function(){console.log("1sec")}, 1000);
console.log("Hi");
Ans:
Hi => 1sec
這看起來可能沒什麼問題,但這個呢,
請問印出的順序為何?
setTimeout(function(){console.log("0sec")}, 0);
console.log("Hi");
沒錯,答案還是
Hi => 0sec
在還未了解到今天講的觀念時,可能會黑人問號,0sec
不是會馬上執行嗎,怎麼還是先印出 Hi
?
好吧,這個問題不太好,因為 HTML5 規定了 setTimeout
的等待時間小於 4ms
時會自動補足。
那我們把問題改成這樣,要等到 3 秒後才會印出 Hi
,這怎麼想 0sec
都會先印出吧?
setTimeout(function(){console.log("0sec")}, 0);
var now = Date.now();
while(true){
if(Date.now() > now + 3000){
break;
}
}
console.log("Hi");
很抱歉,答案還是一樣,等到 3 秒後會印出 Hi
,接著才會再印出 0sec
。
咦,答錯的只有我嗎,你們是不是都答對了呢~
可能觀念還是有點抽象,還記得我們小時候看過的童話書嗎,劇情大概是這樣:
「大家一起努力工作為了完成某件事情,但有些壞人假裝要來工作,其實是來搞破壞的,造成大家進度 delay。國王得知後震怒,下令以後這些壞人一出現,都先把他們隔離,等到他們真的要幹正事時,才能夠加入另外一條次等排隊通道,等到好人們把事情都做完,他們才能夠按照排隊順序進場工作。」
Hmm...我沒有看過這種童話故事,退訂!
且慢,讓我把故事中的一些用語替換掉,
setTimeout
、event
、UI Render
、Ajax
、...。執行 callback function
事件佇列(Queue)
這樣是不是比較清楚了呢?
那我們把剛剛的範例流程列出來,分為兩個主軸,setTimeout
:
setTimeout
被放到旁邊倒數(不管幾秒都會先被拉出來)。callback function
放到 事件佇列(Queue)
。while 等待3秒
:
Hi
等到 主執行緒
執行完所有同步執行的程式碼時,才會去查看 事件佇列
查看有沒有需要被執行的任務,也就是直到這一刻才會印出 0sec
。
觀察到另外一個重點了嗎?
「setTimeout
、setInterval
設定的等待時間,並不能夠確保它真的會在設定的時間到就馬上執行,也就是假設時間為 10sec,這樣只能夠確保它會在 大於等於 10sec 後才會執行」。
都了解以後再看看這個問題,印出的順序為何呢?
setTimeout(function(){console.log("0sec")}, 0);
setTimeout(function(){console.log("2sec")}, 2000);
setTimeout(function(){console.log("4sec")}, 4000);
var now = Date.now();
while(true){
if(Date.now() > now + 3000){
break;
}
}
console.log("Hi");
setTimeout(function(){console.log("3sec")}, 3000);
setTimeout(function(){console.log("0sec")}, 0);
聰明的你應該答對了,
Hi
0sec
2sec
0sec
4sec
3sec
運作流程為:
callback function
丟入 事件佇列
)callback function
丟入 事件佇列
)開發時要避免執行時間過長的同步程式碼,因為不只 setTimeout
、setInterval
,還有其他非同步處理事件,像是 Event
、UI Render
。
請問如果在 3 秒內觸發 click 事件,會發生什麼事...?
document.body.addEventListener('click', function(){
console.log("Clike!");
});
var now = Date.now();
while(true){
if(Date.now() > now + 3000){
break;
}
}
console.log("Hi");
沒錯,在 3 秒內觸發的 click 並不會馬上印出 Click!
,而是會等到 3 秒後印出 Hi
時,才會印出。
同樣的問題還有這個,按直覺來說,應該會先把 body 清空之後,才會印出 Hi 吧?
document.body.innerHTML = "";
var now = Date.now();
while(true){
if(Date.now() > now + 3000){
break;
}
}
console.log("Hi");
答錯了,因為渲染的事件也被丟到非同步處理,所以會等所有同步的程式碼執行完才會觸發!
今天的分享嘗試使用不同的寫法,想讓冰冷的文字多了那麼一點的溫度,試著不讓內容太過枯燥。
但有些用語可能太過口語化,或是用詞沒有那麼精準,若是有些地方沒解釋清楚,不喜歡的朋友別急著左轉,您可以在下方補充較詳細的說明喔。
那今日的分享到這,我們明天見~